#pragma once

#include <string>
#include <vector>
#include <list>

#include <tr1/unordered_set>

#include <gurobi_c++.h>

using namespace std;
using namespace std::tr1;

#define GRBEXECUTE(x) \
  try{ x; }                                                             \
  catch( GRBException e ) { cerr << __FILE__ << ":" << __LINE__ << ": " << e.getErrorCode() << ": " << e.getMessage() << endl; }


namespace ilp {

  const int CT_NO_MATTER   = 9999;
  const int INVALID_HANDLE = -1;
  
  typedef int                       variable_t;
  typedef int                       constraint_t;

  typedef unordered_set<variable_t> variable_set_t;
  typedef unordered_map<ilp::variable_t, ilp::variable_set_t> vvset_dict_t;

  struct variable_template_t {

    double coefficient;
    double startv;
    double lb, ub;
    bool   is_deleted;
    int    var_type;
    GRBVar handle;
    string name;

    variable_template_t( double _coefficient = 0.0, int _var_type = 0, const string &_name = "?" ) {
      coefficient = _coefficient;
      var_type    = _var_type;
      lb          = 0.0; ub = 1.0; startv = -1.0;
      is_deleted  = false;
      name        = _name;
    }
    
  };
  
  struct constraint_template_t {
  
    vector<variable_t>     variables;
    vector<double>         coefficients;
    int                    lhs, rhs;
    bool                   is_deleted;
    bool                   is_sos1;

    constraint_template_t() { is_sos1 = false; is_deleted = false; lhs = 0; rhs = 0; };
    
    void set( int _lhs, int _rhs ) {
      is_deleted = false; lhs = _lhs; rhs = _rhs;
    }
  
    void set( int _lhs, int _rhs, vector<variable_t> &_vars ) {
      is_deleted   = false; lhs = _lhs; rhs = _rhs;
      variables    = _vars;
      coefficients = vector<double>( _vars.size(), 1.0 );
    }
  
    void set( int _lhs, int _rhs, variable_set_t &_vars ) {
      is_deleted = false; lhs = _lhs; rhs = _rhs;

      for( variable_set_t::iterator iter_v = _vars.begin(); _vars.end() != iter_v; ++iter_v )
        push_back( *iter_v, 1.0 );
      
    }
  
    void set( int _lhs, int _rhs, int _num_vars, ... ) {
      is_deleted = false; lhs = _lhs; rhs = _rhs;

      va_list lhs_vars;
      va_start( lhs_vars, _num_vars );

      int     _orig_num_vars = _num_vars;
    
      while( _num_vars-- > 0 ) {
        variable_t var = va_arg( lhs_vars, variable_t );
        variables.push_back( var );
      }

      _num_vars = _orig_num_vars;
    
      while( _num_vars-- > 0 ) {
        double coe = va_arg( lhs_vars, double );
        coefficients.push_back ( coe );
      }
      
      va_end( lhs_vars );
      
    }

    inline void push_back( variable_t var, double coe ) {
      variables.push_back( var );
      coefficients.push_back( coe );
    }
    
    inline void push_back( const variable_set_t &vars, double coe ) {
      for( variable_set_t::const_iterator iter_var = vars.begin(); vars.end() != iter_var; ++iter_var ) {
        variables.push_back( *iter_var );
        coefficients.push_back( coe );
      }
    }
  
  };

  class solver_callback_t;
  
  class heuristic_callback_base_t {

  public:
    heuristic_callback_base_t() {};
    
    virtual void mipnode( solver_callback_t &p_cb ) = 0;
    
  };
  
  class solver_t {
    GRBEnv                       *m_p_env;
    GRBModel                     *m_p_model;
    
    int                           m_num_variables;
    int                           m_num_constraints;
    bool                          m_has_feasible_solution;
    bool                          m_is_solution_optimal;
    bool                          m_f_use_mip_start;

    vector<variable_template_t>   m_stored_variables;
    vector<constraint_template_t> m_stored_constraints;
    
  public:

    solver_t();
    ~solver_t();

    void flush();
    void solve( ostream&, heuristic_callback_base_t* );
    void release();

    inline constraint_t    add_constraint( constraint_template_t **p_out ) { m_stored_constraints.push_back( constraint_template_t() ); if( NULL != p_out ) *p_out = &m_stored_constraints[m_stored_constraints.size() - 1]; return m_stored_constraints.size() - 1; }
    inline constraint_t    add_sos1( constraint_template_t **p_out ) { m_stored_constraints.push_back( constraint_template_t() ); if( NULL != p_out ) *p_out = &m_stored_constraints[m_stored_constraints.size() - 1]; m_stored_constraints[m_stored_constraints.size() - 1].is_sos1 = true; return m_stored_constraints.size() - 1; }
    inline variable_t      add_variable( variable_template_t **p_out, double coe = 0.0, const string &name = "?" ) { m_stored_variables.push_back( variable_template_t( coe, 0, name ) ); if( NULL != p_out ) *p_out = &m_stored_variables[m_stored_variables.size() - 1]; return m_stored_variables.size() - 1; }
    inline void            del_variable( variable_t var ) { m_stored_variables[var].is_deleted = true; };
    inline void            del_constraint( constraint_t con ) { m_stored_constraints[con].is_deleted = true; };
    
    inline void            fix_variable( variable_t var, double val ) { m_stored_variables[ var ].lb = val; m_stored_variables[ var ].ub = val; };
    inline void            set_initial_value( variable_t var, double val ) { m_stored_variables[ var ].startv = val; }

    inline bool            has_feasible_solution() { return m_has_feasible_solution; }
    inline bool            is_solution_optimal() { return m_is_solution_optimal; }
    
    inline double          get_objfn() { GRBEXECUTE( return m_p_model->get( GRB_DoubleAttr_ObjVal ) ); return 0.0; };
    inline double          get_variable_val( variable_t var ) { GRBEXECUTE( return m_stored_variables[ var ].handle.get( GRB_DoubleAttr_X ) ); return 0.0; };
    
    inline constraint_template_t *get_constraint( constraint_t con ) { return &m_stored_constraints[con]; };

    inline vector<variable_template_t> &get_variables() { return m_stored_variables; }
    inline vector<constraint_template_t> &get_constraints() { return m_stored_constraints; }
    
    inline int             get_num_variables() { return m_num_variables; };
    inline int             get_num_constraints() { return m_num_constraints; };

    inline bool            set_mip_start( bool f_switch ) { m_f_use_mip_start = f_switch; }
    
    inline void            set_timeout( double timeout ) { GRBEXECUTE( m_p_model->getEnv().set( GRB_DoubleParam_TimeLimit, timeout ) ); }
    inline void            set_threads( int nprocs ) { GRBEXECUTE( m_p_model->getEnv().set( GRB_IntParam_Threads, nprocs ) ); }
    inline void            set_cutoff( double cutoff ) { GRBEXECUTE( m_p_model->getEnv().set( GRB_DoubleParam_Cutoff, cutoff ) ); }

    inline bool            is_alive() { return NULL != m_p_env; };
    
  };

  class solver_callback_t : public GRBCallback {
    bool                            m_has_feasible_solution;
    double                          m_current_objbest, m_current_objbnd, m_current_gap;
    ilp::solver_t                  *m_p_solver;
    GRBModel                       *m_p_model;
    ostream                        *m_p_os;
    ilp::heuristic_callback_base_t *m_p_hcb;
    
  public:    
    solver_callback_t( ilp::solver_t *p_solver, GRBModel *p_model, ostream *p_os, ilp::heuristic_callback_base_t *p_hcb ) : m_p_solver( p_solver ), m_p_model( p_model ), m_has_feasible_solution( false ), m_p_os( p_os ), m_p_hcb( p_hcb ) {};

    void callback();
    void update_bounds( double = -1, double = -1, double = -1, double = -1 );

    inline bool   has_feasible_solution()                        { return m_has_feasible_solution; };
    inline double get_var_node_rel( variable_t var )             { return getVarNodeRel( m_p_solver->get_variables()[ var ].handle ); };
    inline void   set_var_solution( variable_t var, double val ) { setVarSolution( m_p_solver->get_variables()[ var ].handle, val ); };
    inline void   add_cut( GRBLinExpr &cut, char sense, double rhs ) { addCut( cut, sense, rhs  ); };
    
  };
    
};
